﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace HexTileBoard
{
    /// <summary>
    /// The dictionaries for the types of objects that can be drawn with instancing.
    /// The models exist in their respective dictionaries.
    /// </summary>
    public static class InstancedDictionaries
    {
        public static GraphicsDevice instancedGraphicsDevice;

        public static bool UpdateQuadtreeCounts = false;

        // Components available for drawing instanced objects in the leaf.
        // These are currently specific to our tile based board
        public static Dictionary<HexTileBase, InstancedDrawing.InstancedModel> baseModels;
        public static Dictionary<HexTileManMadeModifier, InstancedDrawing.InstancedModel> manMadeModifierModels;
        public static Dictionary<HexTileNaturalModifier, InstancedDrawing.InstancedModel> naturalModifierModels;
        public static Dictionary<PlayerColor, InstancedDrawing.InstancedModel> colorModels;

        // The arrays of tile types, to loop through for cleaner drawing
        public static Array baseListProvider = BoardConstructor.GetValues<HexTileBase>(); //System.Enum.GetValues(typeof(HexTileBase));
        public static Array manmadeListProvider = BoardConstructor.GetValues<HexTileManMadeModifier>();//System.Enum.GetValues(typeof(HexTileManMadeModifier));
        public static Array naturalListProvider = BoardConstructor.GetValues<HexTileNaturalModifier>();//System.Enum.GetValues(typeof(HexTileNaturalModifier));
        public static Array colorListProvider = BoardConstructor.GetValues<PlayerColor>();//System.Enum.GetValues(typeof(PlayerColor));
        public static Array resourceListProvider = BoardConstructor.GetValues<HexTileResource>();//System.Enum.GetValues(typeof(HexTileResource));

        public static void LoadDictionaries(ContentManager content, GraphicsDevice graphics)
        {
            // Assign the graphics device for instanced drawing
            instancedGraphicsDevice = graphics;

            // Initialize the dictionaries for holding the instanced models
            baseModels = new Dictionary<HexTileBase, InstancedDrawing.InstancedModel>();
            manMadeModifierModels = new Dictionary<HexTileManMadeModifier, InstancedDrawing.InstancedModel>();
            naturalModifierModels = new Dictionary<HexTileNaturalModifier, InstancedDrawing.InstancedModel>();
            colorModels = new Dictionary<PlayerColor, InstancedDrawing.InstancedModel>();

            // Load all relevant models into appropriate dictionaries
            baseModels.Add(HexTileBase.Grass, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_grass"), 1.0f));
            baseModels.Add(HexTileBase.Sand, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_sand"), 1.0f));
            baseModels.Add(HexTileBase.Water, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_water"), 1.0f));

            //naturalModifierModels.Add(HexTileNaturalModifier.Hill2, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hill1"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Hill2, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hill2"), 1.0f));
            //naturalModifierModels.Add(HexTileNaturalModifier.Hill2, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hill3"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Mountain1, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/mountain1"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Mountain2, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/mountain2"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Mountain3, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/mountain3"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Trees1, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/tree1"), 1.0f));
            //naturalModifierModels.Add(HexTileNaturalModifier.Trees1, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/tree2"), 1.0f));
            naturalModifierModels.Add(HexTileNaturalModifier.Wheat, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/wheat"), 1.0f));

            manMadeModifierModels.Add(HexTileManMadeModifier.DefensiveTower, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/defensive"), 1.0f));
            manMadeModifierModels.Add(HexTileManMadeModifier.InfluenceTower1, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/offensive1"), 1.0f));
            manMadeModifierModels.Add(HexTileManMadeModifier.InfluenceTower2, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/offensive2"), 1.0f));
            manMadeModifierModels.Add(HexTileManMadeModifier.InfluenceTower3, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/offensive3"), 1.0f));
            manMadeModifierModels.Add(HexTileManMadeModifier.Bridge, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/bridge"), 1.0f));

            colorModels.Add(PlayerColor.Blue, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_blue"), 1.0f));
            colorModels.Add(PlayerColor.Red, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_red"), 1.0f));
            colorModels.Add(PlayerColor.Green, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_green"), 1.0f));
            colorModels.Add(PlayerColor.Yellow, new InstancedDrawing.InstancedModel(content.Load<Model>("Models/hex_yellow"), 1.0f));
        }
    }

    /// <summary>
    /// Instanced models are comprised of a single loaded model (through the custom pipeline) and it's bones,
    /// and a number of transforms where the model is located in space, which can be drawn from the List<>()
    /// of the class/struct members that compose the objects.
    /// </summary>
    public static class InstancedDrawing
    {
        public class InstancedModel
        {
            public Matrix[] instanceTransforms;     // The locations in space of each instanced model (their world transforms)
            public Model instancedModel;            // The model loaded for the instanced model
            public Matrix[] instancedModelBones;    // The bones for the instanced model loaded.
            public float modelAlpha;                // the alpha value to use when drawing the model.

            public InstancedModel(Model p_Model, float p_Alpha)
            {
                instancedModel = p_Model;
                instancedModelBones = new Matrix[instancedModel.Bones.Count];
                instancedModel.CopyAbsoluteBoneTransformsTo(instancedModelBones);
                modelAlpha = p_Alpha;

                instanceTransforms = new Matrix[0];
            }
        }

        // To store instance transform matrices in a vertex buffer, we use this custom
        // vertex type which encodes 4x4 matrices as a set of four Vector4 values.
        public static DynamicVertexBuffer instanceVertexBufferGrass;
        public static DynamicVertexBuffer instanceVertexBufferSand;
        public static DynamicVertexBuffer instanceVertexBufferWater;
        public static DynamicVertexBuffer instanceVertexBufferBlue;
        public static DynamicVertexBuffer instanceVertexBufferRed;
        public static DynamicVertexBuffer instanceVertexBufferYellow;
        public static DynamicVertexBuffer instanceVertexBufferGreen;
        public static DynamicVertexBuffer instanceVertexBufferHill2;
        public static DynamicVertexBuffer instanceVertexBufferMountain1, instanceVertexBufferMountain2, instanceVertexBufferMountain3;
        public static DynamicVertexBuffer instanceVertexBufferTrees1;
        public static DynamicVertexBuffer instanceVertexBufferWheat;
        public static DynamicVertexBuffer instanceVertexBufferBridge, instanceVertexBufferDefensiveTower, instanceVertexBufferInfluenceTower1, instanceVertexBufferInfluenceTower2, instanceVertexBufferInfluenceTower3;

        public static VertexDeclaration instanceVertexDeclaration = new VertexDeclaration
        (
            new VertexElement(0, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 0),
            new VertexElement(16, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 1),
            new VertexElement(32, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 2),
            new VertexElement(48, VertexElementFormat.Vector4, VertexElementUsage.BlendWeight, 3)
        );

        // Instanced model rendering.
        public static InstancingTechnique instancingTechnique = InstancingTechnique.HardwareInstancing3x3;
        public enum InstancingTechnique
        {
            HardwareInstancingNoShadow,
            NoInstancingNoShadow,
            NoInstancingOrStateBatchingNoShadow,
            HardwareInstancing,
            HardwareInstancing3x3,
            HardwareInstancing5x5,
            HardwareInstancing7x7,
            NoInstancing,
            NoInstancing3x3,
            NoInstancing5x5,
            NoInstancing7x7,
            NoInstancingOrStateBatching,
            NoInstancingOrStateBatching3x3,
            NoInstancingOrStateBatching5x5,
            NoInstancingOrStateBatching7x7,
        }

        // Shadow Variables
        // The size of the shadow map
        // The larger the size the more detail we will have for our entire scene
        private const int shadowMapWidthHeight = 2048;
        public static RenderTarget2D shadowRenderTarget;
        public static Matrix lightViewProjection;     // ViewProjection matrix from the lights perspective
        private static Vector3 LightDirection = Vector3.Normalize(new Vector3(-1f, 1, 1));

        public static void InitInstancing()
        {
            // Create floating point render target
            shadowRenderTarget = new RenderTarget2D(InstancedDictionaries.instancedGraphicsDevice,
                                                    shadowMapWidthHeight,
                                                    shadowMapWidthHeight,
                                                    false,
                                                    SurfaceFormat.Single,
                                                    DepthFormat.Depth24);
        }

        /// <summary>
        /// Efficiently draws several copies of a piece of geometry using hardware instancing.
        /// </summary>
        public static void DrawModelHardwareInstancing(GraphicsDevice graphics, Model model, Matrix[] modelBones, DynamicVertexBuffer instanceVertexBuffer,
                                         Matrix[] instances, Matrix view, Matrix projection, string technique, bool createShadowMap, float XZScale)
        {
            if (instances.Length == 0)
                return;

            // If we have more instances than room in our vertex buffer, grow it to the neccessary size.
            //if ((instanceVertexBuffer == null) ||
            //    (instances.Length > instanceVertexBuffer.VertexCount))
            //{
                if (instanceVertexBuffer != null)
                    instanceVertexBuffer.Dispose();

                instanceVertexBuffer = new DynamicVertexBuffer(graphics, instanceVertexDeclaration,
                                                               instances.Length, BufferUsage.WriteOnly);
            //}

            // Transfer the latest instance transform matrices into the instanceVertexBuffer.
            instanceVertexBuffer.SetData(instances, 0, instances.Length, SetDataOptions.Discard);

            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    // Tell the GPU to read from both the model vertex buffer plus our instanceVertexBuffer.
                    graphics.SetVertexBuffers(
                        new VertexBufferBinding(meshPart.VertexBuffer, meshPart.VertexOffset, 0),
                        new VertexBufferBinding(instanceVertexBuffer, 0, 1)
                    );

                    graphics.Indices = meshPart.IndexBuffer;

                    // Set up the instance rendering effect.
                    Effect effect = meshPart.Effect;

                    effect.CurrentTechnique = effect.Techniques[technique];

                    //effect.Parameters["World"].SetValue(modelBones[mesh.ParentBone.Index] * Matrix.CreateScale(XZScale, 1.0f, XZScale));
                    effect.Parameters["World"].SetValue(modelBones[mesh.ParentBone.Index]);
                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                    effect.Parameters["LightDirection"].SetValue(LightDirection);
                    effect.Parameters["LightViewProj"].SetValue(lightViewProjection);

                    if (!createShadowMap)
                        effect.Parameters["ShadowMap"].SetValue(shadowRenderTarget);

                    // Draw all the instance copies in a single call.
                    foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Apply();

                        graphics.DrawInstancedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                               meshPart.NumVertices, meshPart.StartIndex,
                                                               meshPart.PrimitiveCount, instances.Length);
                    }
                }
            }
        }


        /// <summary>
        /// Draws several copies of a piece of geometry without using any
        /// special GPU instancing techniques at all. This just does a
        /// regular loop and issues several draw calls one after another.
        /// </summary>
        public static void DrawModelNoInstancing(GraphicsDevice graphics, Model model, Matrix[] modelBones,
                                   Matrix[] instances, Matrix view, Matrix projection, string technique, bool createShadowMap, float XZScale)
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    graphics.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    graphics.Indices = meshPart.IndexBuffer;

                    // Set up the rendering effect.
                    Effect effect = meshPart.Effect;

                    effect.CurrentTechnique = effect.Techniques[technique];

                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                    effect.Parameters["LightDirection"].SetValue(LightDirection);
                    effect.Parameters["LightViewProj"].SetValue(lightViewProjection);

                    if (!createShadowMap)
                        effect.Parameters["ShadowMap"].SetValue(shadowRenderTarget);

                    EffectParameter transformParameter = effect.Parameters["World"];

                    // Draw a single instance copy each time around this loop.
                    for (int i = 0; i < instances.Length; i++)
                    {
                        //transformParameter.SetValue(modelBones[mesh.ParentBone.Index] * Matrix.CreateScale(XZScale, 1.0f, XZScale) * instances[i]);
                        transformParameter.SetValue(modelBones[mesh.ParentBone.Index] * instances[i]);

                        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                        {
                            pass.Apply();

                            graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                                 meshPart.NumVertices, meshPart.StartIndex,
                                                                 meshPart.PrimitiveCount);
                        }
                    }
                }
            }
        }


        /// <summary>
        /// This technique is NOT a good idea! It is only included in the sample
        /// for comparison purposes, so you can compare its performance with the
        /// other more sensible approaches. This uses the exact same shader code
        /// as the preceding NoInstancing technique, but with a key difference.
        /// Where the NoInstancing technique worked like this:
        /// 
        ///     SetRenderStates()
        ///     foreach instance
        ///     {
        ///         Update effect with per-instance transform matrix
        ///         DrawIndexedPrimitives()
        ///     }
        /// 
        /// NoInstancingOrStateBatching works like so:
        /// 
        ///     foreach instance
        ///     {
        ///         Set per-instance transform matrix into the effect
        ///         SetRenderStates()
        ///         DrawIndexedPrimitives()
        ///     }
        ///      
        /// As you can see, this is repeatedly setting the same renderstates.
        /// Not so efficient.
        /// 
        /// In other words, the built-in Model.Draw method is pretty inefficient when
        /// it comes to drawing more than one instance! Even without using any fancy
        /// shader techniques, you can get a significant speed boost just by rearranging
        /// your drawing code to work more like the earlier NoInstancing technique.
        /// </summary>
        public static void DrawModelNoInstancingOrStateBatching(GraphicsDevice graphics, Model model, Matrix[] modelBones,
                                                  Matrix[] instances, Matrix view, Matrix projection, string technique, bool createShadowMap, float XZScale)
        {
            for (int i = 0; i < instances.Length; i++)
            {
                foreach (ModelMesh mesh in model.Meshes)
                {
                    foreach (Effect effect in mesh.Effects)
                    {
                        effect.CurrentTechnique = effect.Techniques[technique];

                        //effect.Parameters["World"].SetValue(modelBones[mesh.ParentBone.Index] * Matrix.CreateScale(XZScale, 1.0f, XZScale) * instances[i]);
                        effect.Parameters["World"].SetValue(modelBones[mesh.ParentBone.Index]);
                        effect.Parameters["View"].SetValue(view);
                        effect.Parameters["Projection"].SetValue(projection);
                        effect.Parameters["LightDirection"].SetValue(LightDirection);
                        effect.Parameters["LightViewProj"].SetValue(lightViewProjection);

                        if (!createShadowMap)
                            effect.Parameters["ShadowMap"].SetValue(shadowRenderTarget);
                    }

                    mesh.Draw();
                }
            }
        }

        /// <summary>
        /// Render the shadow map texture to the screen
        /// </summary>
        public static void DrawShadowMapToScreen()
        {
            //spriteBatch.Begin(0, BlendState.Opaque, SamplerState.PointClamp, null, null);
            //spriteBatch.Draw(shadowRenderTarget, new Rectangle(0, 0, 256, 256), Color.White);
            //spriteBatch.End();

            //GraphicsDevice.Textures[0] = null;
            //GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

        /// <summary>
        /// Creates the WorldViewProjection matrix from the perspective of the 
        /// light using the cameras bounding frustum to determine what is visible 
        /// in the scene.
        /// </summary>
        /// <returns>The WorldViewProjection for the light</returns>
        public static Matrix CreateLightViewProjectionMatrix(BoundingFrustum cameraFrustum)
        {
            // Matrix with that will rotate in points the direction of the light
            Matrix lightRotation = Matrix.CreateLookAt(Vector3.Zero,
                                                       -LightDirection,
                                                       Vector3.Up);

            // Get the corners of the frustum
            Vector3[] frustumCorners = cameraFrustum.GetCorners();

            // Transform the positions of the corners into the direction of the light
            for (int i = 0; i < frustumCorners.Length; i++)
            {
                frustumCorners[i] = Vector3.Transform(frustumCorners[i], lightRotation);
            }

            // Find the smallest box around the points
            BoundingBox lightBox = BoundingBox.CreateFromPoints(frustumCorners);

            Vector3 boxSize = lightBox.Max - lightBox.Min;
            Vector3 halfBoxSize = boxSize * 0.5f;

            // The position of the light should be in the center of the back
            // pannel of the box. 
            Vector3 lightPosition = lightBox.Min + halfBoxSize;
            lightPosition.Z = lightBox.Min.Z;

            // We need the position back in world coordinates so we transform 
            // the light position by the inverse of the lights rotation
            lightPosition = Vector3.Transform(lightPosition,
                                              Matrix.Invert(lightRotation));

            // Create the view matrix for the light
            Matrix lightView = Matrix.CreateLookAt(lightPosition,
                                                   lightPosition - LightDirection,
                                                   Vector3.Up);

            // Create the projection matrix for the light
            // The projection is orthographic since we are using a directional light
            Matrix lightProjection = Matrix.CreateOrthographic(boxSize.X, boxSize.Y,
                                                               -boxSize.Z, boxSize.Z);

            return lightView * lightProjection;
        }
    }
}
